﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

using System.Collections.Generic;
using Tesla.Math;

namespace Tesla.Bounding {
    /// <summary>
    /// Query class used to accumulate pick results and then sort them. Picking involves using an array that is cast into
    /// a scene to test for intersections with objects - usually bounding volumes or primitive (triangle) meshes.
    /// </summary>
    public class PickQuery {
        private bool _primitivePickingEnabled;
        private List<PickResult> _results;
        private PickResultsComparer _comparer;
        private bool _sorted;

        /// <summary>
        /// Gets or sets if primitive picking should be enabled. If enabled and a ray is tested successfully against
        /// an IPickable's bounding volume, a finer ray intersection test is asked to be conducted upon the
        /// IPickable's primitives. By default this value is false.
        /// </summary>
        public bool EnablePrimitivePicking {
            get {
                return _primitivePickingEnabled;
            }
            set {
                _primitivePickingEnabled = value;
            }
        }

        /// <summary>
        /// Gets a pick result by index.
        /// </summary>
        /// <param name="index">Index of the pick result</param>
        /// <returns>Pick result</returns>
        public PickResult this[int index] {
            get {
                return _results[index];
            }
        }

        /// <summary>
        /// Gets the number of pick results contained by the query.
        /// </summary>
        public int Count {
            get {
                return _results.Count;
            }
        }

        /// <summary>
        /// Gets the closest pick result, if it exists.
        /// </summary>
        public PickResult? ClosestPick {
            get {
                if(!_sorted) {
                    Sort();
                }
                if(_results.Count > 0) {
                    return _results[0];
                } else {
                    return null;
                }
            }
        }

        /// <summary>
        /// Gets the farthest pick result, if it exists.
        /// </summary>
        public PickResult? FarthestPick {
            get {
                if(!_sorted) {
                    Sort();
                }
                if(_results.Count > 0) {
                    return _results[_results.Count - 1];
                } else {
                    return null;
                }
            }
        }

        /// <summary>
        /// Creates a new PickQuery object.
        /// </summary>
        public PickQuery() {
            _results = new List<PickResult>();
            _comparer = new PickResultsComparer();
        }

        /// <summary>
        /// Creates a new PickQuery object.
        /// </summary>
        /// <param name="enablePrimPicking">True if primitive picking should be enabled.</param>
        public PickQuery(bool enablePrimPicking) {
            _results = new List<PickResult>();
            _comparer = new PickResultsComparer();
            EnablePrimitivePicking = enablePrimPicking;
        }

        /// <summary>
        /// Adds a ray and target to test an intersection. If the target is pickable,
        /// and an intersection exists, the records are added to the query.
        /// </summary>
        /// <param name="ray">Ray to test with</param>
        /// <param name="pickable">Target to test against</param>
        /// <returns>True if an intersection was found, false otherwise.</returns>
        public bool AddPick(Ray ray, IPickable pickable) {
            if(pickable == null) {
                return false;
            }

            if(pickable.IsPickable) {
                BoundingVolume bv = pickable.WorldBounding;
                BoundingIntersectionRecord bvRecord = new BoundingIntersectionRecord();
                PrimitiveIntersectionRecord primRecord = new PrimitiveIntersectionRecord();
                if(bv != null) {
                    bv.IntersectsWhere(ref ray, out bvRecord);
                } else {
                    return false;
                }

                if(bvRecord.IntersectionCount > 0 && _primitivePickingEnabled) {
                    primRecord = pickable.IntersectsPrimitivesWhere(ray);

                    if(primRecord.IntersectionCount > 0) {
                        _results.Add(new PickResult(ray, pickable, bvRecord, primRecord));
                        _sorted = false;
                        return true;
                    }
                } else if(bvRecord.IntersectionCount > 0) {
                    _results.Add(new PickResult(ray, pickable, bvRecord));
                    _sorted = false;
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Sort all picks in the query based on their distances. If primitive picking is enabled,
        /// the primitive intersections are used, otherwise the bounding intersections are used to sort.
        /// </summary>
        public void Sort() {
            _comparer.UsePrimitiveRecord = false;
            _results.Sort(_comparer);
            _sorted = true;
        }

        /// <summary>
        /// Sort all picks, either by the primitive intersections or bounding intersections.
        /// </summary>
        /// <param name="sortByPrims">True for the pick results to be sorted by primitive intersection records.</param>
        public void Sort(bool sortByPrims) {
            _comparer.UsePrimitiveRecord = sortByPrims;
            _results.Sort(_comparer);
            _sorted = true;
        }

        /// <summary>
        /// Clear all pick results from the query.
        /// </summary>
        public void Clear() {
            _results.Clear();
            _sorted = false;
        }

        private class PickResultsComparer : IComparer<PickResult> {

            private bool _usePrim;

            public bool UsePrimitiveRecord {
                get {
                    return _usePrim;
                }
                set {
                    _usePrim = value;
                }
            }
            #region IComparer<PickResults> Members

            public int Compare(PickResult x, PickResult y) {
                if(_usePrim) {
                    PrimitiveIntersectionRecord? rec1 = x.PrimitiveIntersectionRecord;
                    PrimitiveIntersectionRecord? rec2 = y.PrimitiveIntersectionRecord;
                    //Check if both records exist
                    if(rec1.HasValue && rec2.HasValue) {
                        IntersectionRecord? iRec1 = rec1.Value.ClosestIntersection;
                        IntersectionRecord? iRec2 = rec2.Value.ClosestIntersection;
                        return Compare(iRec1, iRec2);
                        //See if record 1 exists, if so then rec2 must not exist
                    } else if(rec1.HasValue) {
                        return -1;
                        //Check if record 2 exists
                    } else if(rec2.HasValue) {
                        return 1;
                        //Otherwise no records, both the same
                    } else {
                        return 0;
                    }
                } else {
                    BoundingIntersectionRecord? rec1 = x.BoundingIntersectionRecord;
                    BoundingIntersectionRecord? rec2 = y.BoundingIntersectionRecord;
                    //Check if both records exist
                    if(rec1.HasValue && rec2.HasValue) {
                        IntersectionRecord? iRec1 = rec1.Value.ClosestIntersection;
                        IntersectionRecord? iRec2 = rec2.Value.ClosestIntersection;
                        return Compare(iRec1, iRec2);
                        //See if record 1 exists, if so then rec2 must not exist
                    } else if(rec1.HasValue) {
                        return -1;
                        //Check if record 2 exists
                    } else if(rec2.HasValue) {
                        return 1;
                        //Otherwise no records, both the same
                    } else {
                        return 0;
                    }
                }
            }

            private int Compare(IntersectionRecord? x, IntersectionRecord? y) {
                if(x.HasValue && y.HasValue) {
                    float dist1 = x.Value.Distance;
                    float dist2 = y.Value.Distance;
                    //Account for floating point error, if just about equal then return 0
                    if(MathHelper.FuzzyEquals(dist1, dist2)) {
                        return 0;
                    }

                    if(dist1 < dist2) {
                        return -1;
                    } else {
                        return 1;
                    }
                } else if(x.HasValue) {
                    return -1;
                } else if(y.HasValue) {
                    return 1;
                } else {
                    return 0;
                }
            }

            #endregion
        }
    }

}
